package org.andromda.android.net;

import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.HttpVersion;
import org.apache.http.NameValuePair;
import org.apache.http.StatusLine;
import org.apache.http.client.CookieStore;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.params.HttpClientParams;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.cookie.Cookie;
import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.HTTP;

import android.text.TextUtils;
import android.util.Log;

/**
 * <p>
 * Low level utilities to handle a network connection with a remote server.
 * </p>
 * <p>
 * This class delegates authentication problems to an
 * <code>AuthenticationStrategy</code> that must be provided by the calling
 * application. The <code>AuthenticationStrategy</code> provides the correct
 * cookie or url parameter to access the remote system.
 * </p>
 * 
 * @see AuthenticationStrategy
 */
public class NetworkUtils {
	protected static final int TIMEOUT = 12500;

	public static String getRedirectUrl(
			final HttpResponse authEntryPointResponse) {
		String entryPointUrl = null;
		final int status1 = authEntryPointResponse.getStatusLine()
				.getStatusCode();
		if (status1 == HttpStatus.SC_MOVED_TEMPORARILY
				|| status1 == HttpStatus.SC_MOVED_PERMANENTLY) {
			final Header[] headers = authEntryPointResponse
					.getHeaders("Location");
			if (headers != null && headers.length > 0) {
				entryPointUrl = headers[0].getValue();
			}
		}
		return entryPointUrl;
	}

	public static String getRedirectUrlPath(final ConnectionData accData,
			final HttpResponse authEntryPointResponse) {
		String entryPointUrl = getRedirectUrl(authEntryPointResponse);
		if (!TextUtils.isEmpty(entryPointUrl)) {
			entryPointUrl = entryPointUrl.substring(entryPointUrl
					.lastIndexOf(accData.getWebappRoot())
					+ accData.getWebappRoot().length());
		}
		return entryPointUrl;
	}

	private final AuthenticationStrategy authenticationStrategy;

	private final CookieStore cookieStore = new BasicCookieStore();

	public NetworkUtils(final AuthenticationStrategy authStrategy) {
		this.authenticationStrategy = authStrategy;
	}

	public void addCookie(final Cookie cookie) {
		this.cookieStore.addCookie(cookie);
	}

	private String buildRelativeURI(final ConnectionData prefs,
			final String _ticket, final String path) {
		final StringBuilder uri = new StringBuilder();
		final String rootURI = prefs.getWebappRoot();

		if (!path.startsWith(rootURI)) {
			if (rootURI.endsWith("/")) {
				uri.append(rootURI.subSequence(0, rootURI.length() - 2));
			} else {
				uri.append(rootURI);
			}
		}

		uri.append(path);
		if (this.authenticationStrategy != null && !TextUtils.isEmpty(_ticket)) {
			final String authData = this.authenticationStrategy
					.authenticationGetParameter(uri.toString(), prefs, _ticket);
			if (!TextUtils.isEmpty(authData)) {
				final String sep = path.contains("?") ? "&" : "?";
				uri.append(sep);
				uri.append(authData);
			}
		}
		return uri.toString();
	}

	public void clearCookies() {
		this.cookieStore.clear();
	}

	public boolean followRedirs(final int status, final String currentUrl,
			final String stopPattern) {
		boolean follow = status == HttpStatus.SC_MOVED_TEMPORARILY
				|| status == HttpStatus.SC_MOVED_PERMANENTLY;
		if (!TextUtils.isEmpty(stopPattern)) {
			follow &= (!currentUrl.matches(stopPattern));
		}
		return follow;
	}

	public String formAuthenticate(final ConnectionData accData,
			final String startUrl, final String successPattern,
			final Map<String, String> authEntryPointFormMap,
			final Map<String, List<NameValuePair>> authEntryPointParameterMap)
			throws NetworkException {
		String currentUrl = startUrl;
		int status = HttpStatus.SC_MOVED_TEMPORARILY;
		while (followRedirs(status, currentUrl, null)) {
			final HttpResponse authEntryPointResponse = makeHttpRequest1(
					accData, null, NetworkMethodEnum.GET, currentUrl,
					(AbstractHttpEntity) null, null);
			status = authEntryPointResponse.getStatusLine().getStatusCode();
			final String nextUrl = getNextUrl(accData, currentUrl,
					authEntryPointResponse);
			if (nextUrl.equals(currentUrl)) {
				break;
			} else {
				currentUrl = nextUrl;
			}
		}

		List<NameValuePair> values = null;
		for (final String pattern : authEntryPointParameterMap.keySet()) {
			if (currentUrl.matches(pattern)) {
				values = authEntryPointParameterMap.get(pattern);
				break;
			}
		}

		String formUrl = null;
		for (final String pattern : authEntryPointFormMap.keySet()) {
			if (currentUrl.matches(pattern)) {
				formUrl = authEntryPointFormMap.get(pattern);
				break;
			}
		}

		try {
			final HttpResponse redirectAfterAuth = makeHttpRequest1(accData,
					null, NetworkMethodEnum.POST, formUrl,
					new UrlEncodedFormEntity(values, HTTP.UTF_8), null);
			currentUrl = getNextUrl(accData, formUrl, redirectAfterAuth);
		} catch (final UnsupportedEncodingException e2) {
			e2.printStackTrace();
		}
		// this should be extracted into the strategy.
		String result = null;
		if (currentUrl.matches(successPattern)) {
			final List<Cookie> cookies = this.cookieStore.getCookies();
			for (final Cookie cookie : cookies) {
				if ("jsessionid".equalsIgnoreCase(cookie.getName())) {
					result = cookie.getValue();
					final String path = cookie.getPath();
					System.out.println(path + "   " + cookie.getName());
					break;
				}
			}
		} else {
			throw new NetworkException(NetworkStatus.CREDENTIALS_ERROR);
		}

		return result;
	}

	public InputStream get(final ConnectionData connectionData,
			final String ticket, final String path) throws NetworkException {
		return makeHttpRequest(connectionData, ticket, path);
	}

	public List<Cookie> getCookies() {
		return this.cookieStore.getCookies();
	}

	private String getNextUrl(final ConnectionData accData,
			final String currentUrl, final HttpResponse authEntryPointResponse) {
		final String redir = getRedirectUrlPath(accData, authEntryPointResponse);

		return (TextUtils.isEmpty(redir) ? currentUrl : redir);
	}

	public InputStream makeHttpRequest(final ConnectionData preferences,
			final String ticket, final NetworkMethodEnum method,
			final String path, final AbstractHttpEntity payLoad,
			final String contentType) throws NetworkException {

		InputStream result = null;
		try {
			final HttpResponse response = makeHttpRequest1(preferences, ticket,
					method, path, payLoad, contentType);
			final StatusLine status = response.getStatusLine();
			final HttpEntity entity = response.getEntity();
			final int statusCode = status.getStatusCode();

			if ((statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_CREATED)
					&& entity != null) {
				result = entity.getContent();
			} else if (statusCode == HttpStatus.SC_UNAUTHORIZED) {
				throw new NetworkException(NetworkStatus.CREDENTIALS_ERROR);
			}
		} catch (final NetworkException e) {
			throw e;
		} catch (final Exception ex) {
			throw new NetworkException(NetworkStatus.CONNECTION_ERROR, method
					+ " method error", ex);
		}
		return result;
	}

	public InputStream makeHttpRequest(final ConnectionData connectionData,
			final String ticket, final NetworkMethodEnum method,
			final String path, final String payLoad, final String contentType)
			throws NetworkException {
		final InputStream result = makeHttpRequest(connectionData, ticket,
				method, path, stringToStringEntity(payLoad), contentType);

		return result;
	}

	public InputStream makeHttpRequest(final ConnectionData connectionData,
			final String ticket, final String path) throws NetworkException {
		return makeHttpRequest(connectionData, ticket, NetworkMethodEnum.GET,
				path, (String) null, null);
	}

	public HttpResponse makeHttpRequest1(final ConnectionData connectionData,
			final String ticket, final NetworkMethodEnum method,
			final String path, final AbstractHttpEntity payLoad,
			final String contentType) throws NetworkException {
		final HttpParams params = new BasicHttpParams();
		HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
		HttpProtocolParams.setContentCharset(params, "utf-8");
		HttpClientParams.setRedirecting(params, false);
		params.setBooleanParameter("http.protocol.expect-continue", false);
		params.setParameter("http.connection.timeout", new Integer(TIMEOUT));
		// registers schemes for both http and https
		final SchemeRegistry registry = new SchemeRegistry();
		registry.register(new Scheme("http", PlainSocketFactory
				.getSocketFactory(), connectionData.getPort()));
		registry.register(new Scheme("https", new EasySSLSocketFactory(),
				connectionData.getPort()));
		final ThreadSafeClientConnManager manager = new ThreadSafeClientConnManager(
				params, registry);

		HttpClient client;
		HttpRequestBase request;
		try {
			final String url = new URL(connectionData.isSsl() ? "https"
					: "http", connectionData.getHostName(),
					connectionData.getPort(), buildRelativeURI(connectionData,
							ticket, path)).toString();
			client = new DefaultHttpClient(manager, params);
			if (this.authenticationStrategy != null && ticket != null) {
				this.authenticationStrategy.setAuthCookies(connectionData,
						ticket, this.cookieStore);
			}
			((DefaultHttpClient) client).setCookieStore(this.cookieStore);

			request = null;
			switch (method) {
			case POST:
				request = new HttpPost(url);
				((HttpPost) request).setEntity(payLoad);
				break;
			case GET:
				request = new HttpGet(url);
				break;
			case PUT:
				request = new HttpPut(url);
				((HttpPut) request).setEntity(payLoad);
				break;
			default:
				throw new NetworkException(NetworkStatus.UNKNOWN_ERROR, -1,
						"Error preparing request: HTTP_METHOD not yet implemented");
			}
		} catch (final Exception e1) {
			throw new NetworkException(NetworkStatus.UNKNOWN_ERROR,
					"Error preparing request", e1);
		}

		if (contentType != null) {
			request.setHeader("Content-type", contentType);
		}

		final HttpResponse response;
		try {
			response = client.execute(request);
		} catch (final Exception ex) {
			throw new NetworkException(NetworkStatus.CONNECTION_ERROR, method
					+ " method error", ex);
		}
		final StatusLine status = response.getStatusLine();
		final int statusCode = status.getStatusCode();

		if (statusCode < 400) { // ok & redirects
			Log.d("NET", "Connection response [" + statusCode
					+ "] status line [" + status.getReasonPhrase() + "]");
			this.authenticationStrategy.checkUnauthorizedResponse(this,
					response);
		} else if (statusCode == HttpStatus.SC_UNAUTHORIZED
				|| statusCode == HttpStatus.SC_FORBIDDEN) {
			throw new NetworkException(NetworkStatus.CREDENTIALS_ERROR,
					statusCode, status.getReasonPhrase());
		} else {
			Log.d("NET",
					"Response [" + statusCode + "] status line ["
							+ status.getReasonPhrase() + "]");
			throw new NetworkException(NetworkStatus.UNKNOWN_ERROR, statusCode,
					status.getReasonPhrase());
		}
		return response;
	}

	public InputStream post(final ConnectionData connectionData,
			final String ticket, final String path, final String payload,
			final String contentType) throws NetworkException {
		return makeHttpRequest(connectionData, ticket, NetworkMethodEnum.POST,
				path, payload, contentType);
	}

	public InputStream put(final ConnectionData connectionData,
			final String ticket, final String path, final String payload,
			final String contentType) throws NetworkException {
		return makeHttpRequest(connectionData, ticket, NetworkMethodEnum.PUT,
				path, payload, contentType);
	}

	public String simpleFormAuthenticate(final ConnectionData accData,
			final String startUrl, final String authForm,
			final Map<String, String> fieldValueMap) throws NetworkException {
		final Map<String, List<NameValuePair>> authEntryPointParameterMap = new HashMap<String, List<NameValuePair>>();
		final List<NameValuePair> nameValues = new ArrayList<NameValuePair>();
		for (final String key : fieldValueMap.keySet()) {
			nameValues.add(new BasicNameValuePair(key, fieldValueMap.get(key)));
		}
		authEntryPointParameterMap.put(".*", nameValues);
		final Map<String, String> authEntryPointFormMap = new HashMap<String, String>();
		authEntryPointFormMap.put(".*", authForm);
		return formAuthenticate(accData, startUrl, startUrl.split("\\?")[0]
				+ ".*", authEntryPointFormMap, authEntryPointParameterMap);
	}

	private StringEntity stringToStringEntity(final String payLoad)
			throws NetworkException {
		final StringEntity stringEntity;
		if (payLoad != null) {
			try {
				stringEntity = new StringEntity(payLoad);
			} catch (final UnsupportedEncodingException e) {
				throw new NetworkException(NetworkStatus.UNKNOWN_ERROR,
						"Error preparing request", e);
			}
		} else {
			stringEntity = null;
		}
		return stringEntity;
	}
}
